##########################################
## Copyright 2012-2013 Ceruti Francesco & contributors
##
## This file is part of LiSP (Linux Show Player).
##########################################

import gi
gi.require_version('Gst', '1.0')
import time
import math
import os
from copy import deepcopy
from threading import Thread, Timer
from lisp.core.media import Media
from gi.repository import Gst
from lisp.utils import audio_utils, configuration as cfg
from PyQt4.QtCore import pyqtSignal


class GstTrack(Media):

    # GstTrack's configuration parameters
    Media.MEDIA_DEFAULT_CONF.update({'volume': {'volume': 1, 'mute': False},
                             'equalizer': {'band0': 0,
                                           'band1': 0,
                                           'band2': 0,
                                           'band3': 0,
                                           'band4': 0,
                                           'band5': 0,
                                           'band6': 0,
                                           'band7': 0,
                                           'band8': 0,
                                           'band9': 0},
                             'pitch': {'tempo': 1,
                                       'pitch': 1},
                             'audiodynamic': {'mode': 'compressor',
                                              'characteristics': 'hard-knee',
                                              'ratio': 1,
                                              'threshold': 1},
                             'fadeIn': {'time': 0,
                                        'exp': 1.5},
                             'fadeOut': {'time': 0,
                                         'exp': 3},
                             'duration': -1,
                             'mtime': -1,
                             'loop': False,
                             'wait': 0,
                             'endFadeOut': False,
                             'pipeline': cfg.config['GStreamer']['DefaultPipeline']})

    onLevel = pyqtSignal(list, list)

    def __init__(self, conf=None, uuid=None):
        super(GstTrack, self).__init__(uuid)

        self.gstState = Gst.State.NULL
        self.dPipe = {}
        self.gstPipe = None

        self.conf.update(deepcopy(Media.MEDIA_DEFAULT_CONF))
        self.createPipeline()
        self.updateConfiguration(conf)

    def createPipeline(self):
        if(self.state != GstTrack.NONE):
            self.interrupt(False)

        self.gstPipe = Gst.parse_launch(self.conf['pipeline'])

        # Put the various elements of pipeline in a dict
        for index in range(self.gstPipe.get_children_count()):
            elem = self.gstPipe.get_child_by_index(index)
            self.dPipe[elem.get_property('name')] = elem

        # Get the pipeline bus and link it to __on_message function
        bus = self.gstPipe.get_bus()
        bus.add_signal_watch()
        bus.connect("message", self.__on_message)

    def updateConfiguration(self, conf, emit=True, duration=True):
        if(conf is not None):
            if('pipeline' in conf):
                if(self.conf['pipeline'] != conf['pipeline']):
                    self.conf['pipeline'] = conf['pipeline']
                    self.createPipeline()

            self.conf.update(conf)

            temp_location = None
            if('location' in conf and (self.state == Media.PLAYING or self.state == Media.PAUSED)):
                temp_location = conf.pop('location')

            # Set elements properties, using conf, not self.conf
            for el in self.dPipe:
                if(el in conf):
                    if(isinstance(conf[el], dict)):
                        for pr in conf[el]:
                            self.dPipe[el].set_property(pr, conf[el][pr])
                    else:
                        self.dPipe[el].set_property(el, conf[el])

            if(temp_location is not None):
                self.conf['location'] = temp_location

            if(duration and os.path.exists(self.conf['location'])):
                if(self.conf['mtime'] != os.path.getmtime(self.conf['location']) or self.conf['duration'] == -1):
                    self.conf['mtime'] = os.path.getmtime(self.conf['location'])
                    fext = audio_utils.extension(self.conf['location'])
                    if(fext == '.wav' or fext == '.wave'):
                        self.__duration()
                    else:
                        thread = Thread(target=self.__duration)
                        thread.setDaemon(True)
                        thread.start()
                else:
                    self.onDuration.emit(self, self.conf['duration'])
            elif(not os.path.exists(self.conf['location'])):
                raise Exception('Audio file not exists: ' + self.conf['location'])

            if(self.state == GstTrack.NONE):
                self.state = GstTrack.STOPPED

            if(emit):
                self.onConfigurationChanged.emit(self)

    def play(self, emit=True):
        # If state is not equals to STOPPED or PAUSED the track cannot be played
        if(self.state == GstTrack.STOPPED or self.state == GstTrack.PAUSED):
            self.state = GstTrack.PLAYING
            # Reset the volume property
            self.dPipe['volume'].set_property('volume', self.conf['volume']['volume'])

            if(emit):
                self.onPlay.emit(self)
            if(self.conf['wait'] > 0):
                Timer(self.conf['wait'], self.__play_process).start()
            else:
                self.__play_process()

    def stop(self, emit=True):
        # If state is not equals to PLAYING or PAUSED the track cannot be stopped
        if(self.state == GstTrack.PLAYING or self.state == GstTrack.PAUSED):
            self.state = GstTrack.STOPPED

            self.stop_thread = Thread(target=self.__stop_process, args=(emit,))
            self.stop_thread.setDaemon(True)

            if(emit):
                self.onStop.emit(self)
            self.stop_thread.start()

    def pause(self, emit=True):
        # If state is not equals to PLAYING the track cannot be paused
        if(self.state == GstTrack.PLAYING):
            self.state = GstTrack.PAUSED

            self.stop_thread = Thread(target=self.__pause_process, args=(emit,))
            self.stop_thread.setDaemon(True)

            if(emit):
                self.onPause.emit(self)
            self.stop_thread.start()

    def seek(self, seek_time, emit=True):
        # If state is NONE the track cannot be sought
        if(self.state != GstTrack.NONE):
            if(seek_time < self.conf['duration']):
                if(self.state == GstTrack.STOPPED or self.state == GstTrack.PAUSED):
                    self.play()
                else:
                    self.gstPipe.seek_simple(Gst.Format.TIME, Gst.SeekFlags.FLUSH, seek_time * Gst.MSECOND)

        if(emit):
            self.onSeek.emit(self, seek_time)

    def currentTime(self):
        return int(self.gstPipe.query_position(Gst.Format.TIME)[1] / Gst.MSECOND)

    def interrupt(self, reconf=True):
        self.gstPipe.set_state(Gst.State.NULL)
        self.state = GstTrack.NONE
        if(reconf):
            self.createPipeline()
            self.updateConfiguration(self.conf, emit=False, duration=False)

    def __play_process(self):
        # If fadeIn is > 0 start the track with the fadeIn thread
        if(self.conf['fadeIn']['time'] > 0):
            self.dPipe['volume'].set_property('volume', 0)
            self.fadeIn_thread = Thread(target=self.__fadein)
            self.fadeIn_thread.setDaemon(True)
            self.gstPipe.set_state(Gst.State.PLAYING)
            self.fadeIn_thread.start()
        else:
            self.gstPipe.set_state(Gst.State.PLAYING)

        # If endFadeOut is enabled and fadeOut is > 0 start endFadeOut thread
        if(self.conf['endFadeOut'] and self.conf['fadeOut']['time'] > 0):
            self.endFadeOut_thread = Thread(target=self.__endfadeout)
            self.endFadeOut_thread.setDaemon(True)
            self.endFadeOut_thread.start()

    def __pause_process(self, emit):
        pause = True
        if(self.conf['fadeOut']['time'] > 0):
            pause = self.__fadeout()
        if(pause):
            self.gstPipe.set_state(Gst.State.PAUSED)
            if(emit):
                self.onPaused.emit(self)

    def __stop_process(self, emit):
        stop = True
        if(self.conf['fadeOut']['time'] > 0):
            stop = self.__fadeout()
        if(stop):
            self.interrupt()
            if(emit):
                self.onStopped.emit(self)

    def __on_finish(self):
        self.interrupt()

        if(self.conf['loop'] == True):
            self.play()
        else:
            self.state = GstTrack.STOPPED
            self.onEndOfStream.emit(self)

    def __endfadeout(self):
        while(self.state == GstTrack.PLAYING or self.gstState == Gst.State.PLAYING):
            # If fadeOut (enabled) is > 0 and >= of remaining time start fadeOut thread
            if(self.conf['fadeOut']['time'] >= self.conf['duration'] / 100 - self.currentTime() / 100):
                self.state = GstTrack.STOPPED
                self.__fadeout()
            time.sleep(0.1)

    def __fadein(self):
        self.onFadeIn.emit(True)
        sec = 0
        while(sec < self.conf['fadeIn']['time']):
            if(self.state != GstTrack.PLAYING):
                return False
            self.dPipe['volume'].set_property('volume', (math.pow(sec, self.conf['fadeIn']['exp']) / math.pow(self.conf['fadeIn']['time'], self.conf['fadeIn']['exp'])) * self.conf['volume']['volume'])
            sec += self.conf['fadeIn']['time'] / 1000
            time.sleep(self.conf['fadeIn']['time'] / 10000)
        self.onFadeIn.emit(False)
        return True

    def __fadeout(self):
        self.onFadeOut.emit(True)
        sec = self.conf['fadeOut']['time']
        # If fading out while still in fade-in it would differ, audibly, from self.conf['volume']['volume']
        currentVolume = self.dPipe['volume'].get_property('volume')
        while(sec >= 0):
            if(self.state == GstTrack.PLAYING or self.state == GstTrack.NONE):
                return False
            self.dPipe['volume'].set_property('volume', (math.pow(sec, self.conf['fadeOut']['exp']) / math.pow(self.conf['fadeOut']['time'], self.conf['fadeOut']['exp'])) * currentVolume)
            sec -= self.conf['fadeOut']['time'] / 1000
            time.sleep(self.conf['fadeOut']['time'] / 10000)
        self.onFadeOut.emit(False)
        return True

    # Grab and process the messages on the pipeline bus
    def __on_message(self, bus, message):
        if(message.type == Gst.MessageType.ERROR):
            self.interrupt()
            err, debug = message.parse_error()
            print(str(err) + '\n' + str(debug))
            self.onError.emit(self, str(err) + '\n' + str(debug))
        elif(message.type == Gst.MessageType.EOS):
            self.__on_finish()
        elif(message.type == Gst.MessageType.STATE_CHANGED):
            self.gstState = message.parse_state_changed()[1]
        elif(message.type == Gst.MessageType.ELEMENT):
            struct = message.get_structure()
            if(struct is not None):
                if(struct.has_name('level')):
                    self.onLevel.emit(struct.get_value('peak'), struct.get_value('decay'))

    def __duration(self):
        self.conf['duration'] = int(audio_utils.getDuration(self.conf['location']))
        if(self.conf['duration'] == -1):
            raise Exception('Audio file not readable: ' + self.conf['location'])

        self.onDuration.emit(self, self.conf['duration'])
